import {Injectable} from '@angular/core';
import {
    BehaviorSubject,
    from,
    Observable,
    Observer,
    of,
    Subject,
    Subscriber
} from 'rxjs';
import {filter, first, tap} from 'rxjs/operators'
import {CourseCheck} from '../course/Course';
import {
    PythonExceptionMessage,
    PythonFinishMessage,
    PythonInputMessage,
    PythonMessage,
    PythonOutputMessage
} from './PythonMessage';
declare let loadPyodide: any;

@Injectable({providedIn: 'root'})
export class PyodideService {

    pyodide : any = null;

    constructor(private window : Window) {}
    state = "unloaded";
    loadingState = new BehaviorSubject<string>("unloaded");
    async loadPyodideAsync() {
        return await loadPyodide({indexURL: "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/"});
    }
    load(): Observable < any > {
        if (this.state === 'unloaded') {
            this.state = "loading";
            this.loadingState.next(this.state);
            return from(this.loadPyodideAsync()).pipe(tap(p => {
                this.pyodide = p;
                this.state = "loaded";
                this.loadingState.next("loaded")
            }))
        } else {
            return of(this.pyodide)
        }
    }
    async runAsync(code : string, clear = false) {
        if (this.state === 'unload') {
            await this.loadPyodideAsync();
        } else if (this.state === 'loading') {
            await this.loadingState.asObservable().pipe(filter(x => x === 'loaded'), first()).toPromise();
        }
        if (clear) {
            const dict = this.pyodide.pyimport("dict");
            return this.pyodide.runPythonAsync(code, dict());
        } else {
            return this.pyodide.runPythonAsync(code);
        }
    }
    run(code : string, clear = false) {
        return from(this.runAsync(code, clear));
    }
    async redirect(input : ((text : string) => string), print : (text : string, sep : string, end : string) => any) {
        if (this.state === 'unload') {
            await this.loadPyodideAsync();
        } else if (this.state === 'loading') {
            await this.loadingState.asObservable().pipe(filter(x => x === 'loaded'), first()).toPromise();
        }
        (this.window as any).pyodide_input_modified = (text : string) => input(text);
        (this.window as any).pyodide_print_modified = (text : string, sep : string, end : string) => print(text, sep, end);
        return await this.runAsync(`from js import pyodide_input_modified, pyodide_print_modified
input = pyodide_input_modified
print = pyodide_print_modified
__builtins__.input = pyodide_input_modified
__builtins__.print = pyodide_print_modified`)
    }
    async simpleRun(code : string, preinputLines : string[]): Promise < Observable < PythonMessage >> {
        const subject = new Subject<PythonMessage>();
        // TODO 有歧义，必须采取以下策略之一(1)必须以回车结尾(2)增加允许输入区的开关(3)添加吞空回车和解决办法的说明
        let inputLineId = 0;
        await this.redirect((text) => {
            if (inputLineId < preinputLines.length) {
                return preinputLines[inputLineId++];
            } else {
                return prompt(text) ?? ''
            }
        }, (text, sep, end = '\n') => {
            subject.next(new PythonOutputMessage(text + end))
        });
        this.run(code).subscribe(x => subject.next(new PythonFinishMessage()), error => subject.next(new PythonExceptionMessage(error)));
        return subject;
    }
    async check(code : string, check : CourseCheck): Promise < boolean > {
        for (let i = 0; i < check.time; ++ i) { // TODO 这里没有检查输入数量对不上的情况
            try {
                const result = await this.runAsync(`
from js import pyodide_input_lines
pyodide_check_time = ${i}

${
                    check.before
                }
pyodide_input_line_index = 0
pyodide_output_text = ''
def pyodide_input_check(text = ''):
    global pyodide_input_line_index
    if pyodide_input_line_index >= len(pyodide_input_lines):
        return ''
    pyodide_input_line_index += 1
    return pyodide_input_lines[pyodide_input_line_index-1]
def pyodide_print_check(text, sep=' ', end='\\n'):
    global pyodide_output_text
    pyodide_output_text += str(text) + end
input = pyodide_input_check
print = pyodide_print_check
__builtins__.input = pyodide_input_check
__builtins__.print = pyodide_print_check
${code}
${
                    check.after
                }`)
                if (! result) {
                    return false;
                }
            } catch(e) {
                console.error(e);
                return false;
            }}
        return true;
    }
    async test(code : string, test : CourseCheck): Promise < Observable < PythonMessage >> {
        const subject = new Subject < PythonMessage > ();
        for (let i = 0; i < test.time; ++i) { // TODO 这里没有检查输入数量对不上的情况
            this.run(`
pyodide_test_time = ${i}
${
                test.before
            }
pyodide_input_line_index = 0
pyodide_output_text = ''
def pyodide_input_test(text = ''):
    global pyodide_input_line_index
    if pyodide_input_line_index >= len(pyodide_input_lines):
        return ''
    pyodide_input_line_index += 1
    return pyodide_input_lines[pyodide_input_line_index-1]
def pyodide_print_test(text, sep=' ', end='\\n'):
    global pyodide_output_text
    pyodide_output_text += str(text) + end
input = pyodide_input_test
print = pyodide_print_test
__builtins__.input = pyodide_input_test
__builtins__.print = pyodide_print_test
${code}
`).subscribe(x => {
                from(this.redirect((text) => '', (text) => subject.next(new PythonOutputMessage(text + '\n')))).subscribe(z => this.run(test.after).subscribe(zz => subject.next(new PythonFinishMessage())));
            })
        }
        return subject.asObservable();
    }
}
